/*
 * Copyright 2002-2014 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package qunar.tc.bistoury.instrument.client.spring.el;

import java.lang.reflect.Array;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Base {@link ConversionService} implementation suitable for use in most environments.
 * Indirectly implements {@link ConverterRegistry} as registration API through the
 * {@link ConfigurableConversionService} interface.
 *
 * @author Keith Donald
 * @author Juergen Hoeller
 * @author Chris Beams
 * @author Phillip Webb
 * @since 3.0
 */
class GenericConversionService implements ConfigurableConversionService {

    /**
     * General NO-OP converter used when conversion is not required.
     */
    private static final GenericConverter NO_OP_CONVERTER = new NoOpConverter("NO_OP");

    /**
     * Used as a cache entry when no converter is available.  This converter is never
     * returned.
     */
    private static final GenericConverter NO_MATCH = new NoOpConverter("NO_MATCH");


    private final Converters converters = new Converters();

    private final Map<ConverterCacheKey, GenericConverter> converterCache =
            new ConcurrentHashMap<ConverterCacheKey, GenericConverter>(64);


    // implementing ConverterRegistry

    public void addConverter(Converter<?, ?> converter) {
        GenericConverter.ConvertiblePair typeInfo = getRequiredTypeInfo(converter, Converter.class);
        Assert.notNull(typeInfo, "Unable to the determine sourceType <S> and targetType " +
                "<T> which your Converter<S, T> converts between; declare these generic types.");
        addConverter(new ConverterAdapter(converter, typeInfo));
    }

    public void addConverter(Class<?> sourceType, Class<?> targetType, Converter<?, ?> converter) {
        GenericConverter.ConvertiblePair typeInfo = new GenericConverter.ConvertiblePair(sourceType, targetType);
        addConverter(new ConverterAdapter(converter, typeInfo));
    }

    public void addConverter(GenericConverter converter) {
        this.converters.add(converter);
        invalidateCache();
    }

    public void addConverterFactory(ConverterFactory<?, ?> converterFactory) {
        GenericConverter.ConvertiblePair typeInfo = getRequiredTypeInfo(converterFactory, ConverterFactory.class);
        if (typeInfo == null) {
            throw new IllegalArgumentException("Unable to the determine sourceType <S> and " +
                    "targetRangeType R which your ConverterFactory<S, R> converts between; " +
                    "declare these generic types.");
        }
        addConverter(new ConverterFactoryAdapter(converterFactory, typeInfo));
    }

    public void removeConvertible(Class<?> sourceType, Class<?> targetType) {
        this.converters.remove(sourceType, targetType);
        invalidateCache();
    }

    // implementing ConversionService

    public boolean canConvert(Class<?> sourceType, Class<?> targetType) {
        Assert.notNull(targetType, "targetType to convert to cannot be null");
        return canConvert((sourceType != null ? TypeDescriptor.valueOf(sourceType) : null),
                TypeDescriptor.valueOf(targetType));
    }

    public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {
        Assert.notNull(targetType, "targetType to convert to cannot be null");
        if (sourceType == null) {
            return true;
        }
        GenericConverter converter = getConverter(sourceType, targetType);
        return (converter != null);
    }

    /**
     * Returns true if conversion between the sourceType and targetType can be bypassed.
     * More precisely this method will return true if objects of sourceType can be
     * converted to the targetType by returning the source object unchanged.
     *
     * @param sourceType context about the source type to convert from (may be null if source is null)
     * @param targetType context about the target type to convert to (required)
     * @return true if conversion can be bypassed
     * @throws IllegalArgumentException if targetType is null
     * @since 3.2
     */
    public boolean canBypassConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {
        Assert.notNull(targetType, "The targetType to convert to cannot be null");
        if (sourceType == null) {
            return true;
        }
        GenericConverter converter = getConverter(sourceType, targetType);
        return (converter == NO_OP_CONVERTER);
    }

    @SuppressWarnings("unchecked")
    public <T> T convert(Object source, Class<T> targetType) {
        Assert.notNull(targetType, "The targetType to convert to cannot be null");
        return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType));
    }

    public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
        Assert.notNull(targetType, "The targetType to convert to cannot be null");
        if (sourceType == null) {
            Assert.isTrue(source == null, "The source must be [null] if sourceType == [null]");
            return handleResult(sourceType, targetType, convertNullSource(sourceType, targetType));
        }
        if (source != null && !sourceType.getObjectType().isInstance(source)) {
            throw new IllegalArgumentException("The source to convert from must be an instance of " +
                    sourceType + "; instead it was a " + source.getClass().getName());
        }
        GenericConverter converter = getConverter(sourceType, targetType);
        if (converter != null) {
            Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
            return handleResult(sourceType, targetType, result);
        }
        return handleConverterNotFound(source, sourceType, targetType);
    }

    /**
     * Convenience operation for converting a source object to the specified targetType,
     * where the targetType is a descriptor that provides additional conversion context.
     * Simply delegates to {@link #convert(Object, TypeDescriptor, TypeDescriptor)} and
     * encapsulates the construction of the sourceType descriptor using
     * {@link TypeDescriptor#forObject(Object)}.
     *
     * @param source     the source object
     * @param targetType the target type
     * @return the converted value
     * @throws ConversionException      if a conversion exception occurred
     * @throws IllegalArgumentException if targetType is null,
     *                                  or sourceType is null but source is not null
     */
    public Object convert(Object source, TypeDescriptor targetType) {
        return convert(source, TypeDescriptor.forObject(source), targetType);
    }

    @Override
    public String toString() {
        return this.converters.toString();
    }


    // subclassing hooks

    /**
     * Template method to convert a null source.
     * <p>Default implementation returns {@code null}.
     * Subclasses may override to return custom null objects for specific target types.
     *
     * @param sourceType the sourceType to convert from
     * @param targetType the targetType to convert to
     * @return the converted null object
     */
    protected Object convertNullSource(TypeDescriptor sourceType, TypeDescriptor targetType) {
        return null;
    }

    /**
     * Hook method to lookup the converter for a given sourceType/targetType pair.
     * First queries this ConversionService's converter cache.
     * On a cache miss, then performs an exhaustive search for a matching converter.
     * If no converter matches, returns the default converter.
     * Subclasses may override.
     *
     * @param sourceType the source type to convert from
     * @param targetType the target type to convert to
     * @return the generic converter that will perform the conversion, or {@code null} if
     * no suitable converter was found
     * @see #getDefaultConverter(TypeDescriptor, TypeDescriptor)
     */
    protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
        ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType);
        GenericConverter converter = this.converterCache.get(key);
        if (converter != null) {
            return (converter != NO_MATCH ? converter : null);
        }

        converter = this.converters.find(sourceType, targetType);
        if (converter == null) {
            converter = getDefaultConverter(sourceType, targetType);
        }

        if (converter != null) {
            this.converterCache.put(key, converter);
            return converter;
        }

        this.converterCache.put(key, NO_MATCH);
        return null;
    }

    /**
     * Return the default converter if no converter is found for the given sourceType/targetType pair.
     * Returns a NO_OP Converter if the sourceType is assignable to the targetType.
     * Returns {@code null} otherwise, indicating no suitable converter could be found.
     * Subclasses may override.
     *
     * @param sourceType the source type to convert from
     * @param targetType the target type to convert to
     * @return the default generic converter that will perform the conversion
     */
    protected GenericConverter getDefaultConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
        return (sourceType.isAssignableTo(targetType) ? NO_OP_CONVERTER : null);
    }

    // internal helpers

    private GenericConverter.ConvertiblePair getRequiredTypeInfo(Object converter, Class<?> genericIfc) {
        Class<?>[] args = GenericTypeResolver.resolveTypeArguments(converter.getClass(), genericIfc);
        return (args != null ? new GenericConverter.ConvertiblePair(args[0], args[1]) : null);
    }

    private void invalidateCache() {
        this.converterCache.clear();
    }

    private Object handleConverterNotFound(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
        if (source == null) {
            assertNotPrimitiveTargetType(sourceType, targetType);
            return source;
        }
        if (sourceType.isAssignableTo(targetType) && targetType.getObjectType().isInstance(source)) {
            return source;
        }
        throw new ConverterNotFoundException(sourceType, targetType);
    }

    private Object handleResult(TypeDescriptor sourceType, TypeDescriptor targetType, Object result) {
        if (result == null) {
            assertNotPrimitiveTargetType(sourceType, targetType);
        }
        return result;
    }

    private void assertNotPrimitiveTargetType(TypeDescriptor sourceType, TypeDescriptor targetType) {
        if (targetType.isPrimitive()) {
            throw new ConversionFailedException(sourceType, targetType, null,
                    new IllegalArgumentException("A null value cannot be assigned to a primitive type"));
        }
    }


    /**
     * Adapts a {@link Converter} to a {@link GenericConverter}.
     */
    @SuppressWarnings("unchecked")
    private final class ConverterAdapter implements ConditionalGenericConverter {

        private final Converter<Object, Object> converter;

        private final ConvertiblePair typeInfo;

        public ConverterAdapter(Converter<?, ?> converter, ConvertiblePair typeInfo) {
            this.converter = (Converter<Object, Object>) converter;
            this.typeInfo = typeInfo;
        }

        public Set<ConvertiblePair> getConvertibleTypes() {
            return Collections.singleton(this.typeInfo);
        }

        public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
            if (!this.typeInfo.getTargetType().equals(targetType.getObjectType())) {
                return false;
            }
            if (this.converter instanceof ConditionalConverter) {
                return ((ConditionalConverter) this.converter).matches(sourceType, targetType);
            }
            return true;
        }

        public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
            if (source == null) {
                return convertNullSource(sourceType, targetType);
            }
            return this.converter.convert(source);
        }

        @Override
        public String toString() {
            return this.typeInfo + " : " + this.converter;
        }
    }


    /**
     * Adapts a {@link ConverterFactory} to a {@link GenericConverter}.
     */
    @SuppressWarnings("unchecked")
    private final class ConverterFactoryAdapter implements ConditionalGenericConverter {

        private final ConverterFactory<Object, Object> converterFactory;

        private final ConvertiblePair typeInfo;

        public ConverterFactoryAdapter(ConverterFactory<?, ?> converterFactory, ConvertiblePair typeInfo) {
            this.converterFactory = (ConverterFactory<Object, Object>) converterFactory;
            this.typeInfo = typeInfo;
        }

        public Set<ConvertiblePair> getConvertibleTypes() {
            return Collections.singleton(this.typeInfo);
        }

        public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
            boolean matches = true;
            if (this.converterFactory instanceof ConditionalConverter) {
                matches = ((ConditionalConverter) this.converterFactory).matches(sourceType, targetType);
            }
            if (matches) {
                Converter<?, ?> converter = this.converterFactory.getConverter(targetType.getType());
                if (converter instanceof ConditionalConverter) {
                    matches = ((ConditionalConverter) converter).matches(sourceType, targetType);
                }
            }
            return matches;
        }

        public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
            if (source == null) {
                return convertNullSource(sourceType, targetType);
            }
            return this.converterFactory.getConverter(targetType.getObjectType()).convert(source);
        }

        @Override
        public String toString() {
            return this.typeInfo + " : " + this.converterFactory;
        }
    }


    /**
     * Key for use with the converter cache.
     */
    private static final class ConverterCacheKey {

        private final TypeDescriptor sourceType;

        private final TypeDescriptor targetType;

        public ConverterCacheKey(TypeDescriptor sourceType, TypeDescriptor targetType) {
            this.sourceType = sourceType;
            this.targetType = targetType;
        }

        public boolean equals(Object other) {
            if (this == other) {
                return true;
            }
            if (!(other instanceof ConverterCacheKey)) {
                return false;
            }
            ConverterCacheKey otherKey = (ConverterCacheKey) other;
            return ObjectUtils.nullSafeEquals(this.sourceType, otherKey.sourceType) &&
                    ObjectUtils.nullSafeEquals(this.targetType, otherKey.targetType);
        }

        @Override
        public int hashCode() {
            return ObjectUtils.nullSafeHashCode(this.sourceType) * 29 +
                    ObjectUtils.nullSafeHashCode(this.targetType);
        }

        @Override
        public String toString() {
            return "ConverterCacheKey [sourceType = " + this.sourceType +
                    ", targetType = " + this.targetType + "]";
        }
    }


    /**
     * Manages all converters registered with the service.
     */
    private static class Converters {

        private final Set<GenericConverter> globalConverters = new LinkedHashSet<GenericConverter>();

        private final Map<GenericConverter.ConvertiblePair, ConvertersForPair> converters =
                new LinkedHashMap<GenericConverter.ConvertiblePair, ConvertersForPair>(36);

        public void add(GenericConverter converter) {
            Set<GenericConverter.ConvertiblePair> convertibleTypes = converter.getConvertibleTypes();
            if (convertibleTypes == null) {
                Assert.state(converter instanceof ConditionalConverter,
                        "Only conditional converters may return null convertible types");
                this.globalConverters.add(converter);
            } else {
                for (GenericConverter.ConvertiblePair convertiblePair : convertibleTypes) {
                    ConvertersForPair convertersForPair = getMatchableConverters(convertiblePair);
                    convertersForPair.add(converter);
                }
            }
        }

        private ConvertersForPair getMatchableConverters(GenericConverter.ConvertiblePair convertiblePair) {
            ConvertersForPair convertersForPair = this.converters.get(convertiblePair);
            if (convertersForPair == null) {
                convertersForPair = new ConvertersForPair();
                this.converters.put(convertiblePair, convertersForPair);
            }
            return convertersForPair;
        }

        public void remove(Class<?> sourceType, Class<?> targetType) {
            this.converters.remove(new GenericConverter.ConvertiblePair(sourceType, targetType));
        }

        /**
         * Find a {@link GenericConverter} given a source and target type.
         * <p>This method will attempt to match all possible converters by working
         * through the class and interface hierarchy of the types.
         *
         * @param sourceType the source type
         * @param targetType the target type
         * @return a matching {@link GenericConverter}, or {@code null} if none found
         */
        public GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) {
            // Search the full type hierarchy
            List<Class<?>> sourceCandidates = getClassHierarchy(sourceType.getType());
            List<Class<?>> targetCandidates = getClassHierarchy(targetType.getType());
            for (Class<?> sourceCandidate : sourceCandidates) {
                for (Class<?> targetCandidate : targetCandidates) {
                    GenericConverter.ConvertiblePair convertiblePair = new GenericConverter.ConvertiblePair(sourceCandidate, targetCandidate);
                    GenericConverter converter = getRegisteredConverter(sourceType, targetType, convertiblePair);
                    if (converter != null) {
                        return converter;
                    }
                }
            }
            return null;
        }

        private GenericConverter getRegisteredConverter(TypeDescriptor sourceType,
                                                        TypeDescriptor targetType, GenericConverter.ConvertiblePair convertiblePair) {

            // Check specifically registered converters
            ConvertersForPair convertersForPair = this.converters.get(convertiblePair);
            if (convertersForPair != null) {
                GenericConverter converter = convertersForPair.getConverter(sourceType, targetType);
                if (converter != null) {
                    return converter;
                }
            }
            // Check ConditionalGenericConverter that match all types
            for (GenericConverter globalConverter : this.globalConverters) {
                if (((ConditionalConverter) globalConverter).matches(sourceType, targetType)) {
                    return globalConverter;
                }
            }
            return null;
        }

        /**
         * Returns an ordered class hierarchy for the given type.
         *
         * @param type the type
         * @return an ordered list of all classes that the given type extends or implements
         */
        private List<Class<?>> getClassHierarchy(Class<?> type) {
            List<Class<?>> hierarchy = new ArrayList<Class<?>>(20);
            Set<Class<?>> visited = new HashSet<Class<?>>(20);
            addToClassHierarchy(0, ClassUtils.resolvePrimitiveIfNecessary(type), false, hierarchy, visited);
            boolean array = type.isArray();
            int i = 0;
            while (i < hierarchy.size()) {
                Class<?> candidate = hierarchy.get(i);
                candidate = (array ? candidate.getComponentType() : ClassUtils.resolvePrimitiveIfNecessary(candidate));
                Class<?> superclass = candidate.getSuperclass();
                if (candidate.getSuperclass() != null && superclass != Object.class) {
                    addToClassHierarchy(i + 1, candidate.getSuperclass(), array, hierarchy, visited);
                }
                for (Class<?> implementedInterface : candidate.getInterfaces()) {
                    addToClassHierarchy(hierarchy.size(), implementedInterface, array, hierarchy, visited);
                }
                i++;
            }
            addToClassHierarchy(hierarchy.size(), Object.class, array, hierarchy, visited);
            addToClassHierarchy(hierarchy.size(), Object.class, false, hierarchy, visited);
            return hierarchy;
        }

        private void addToClassHierarchy(int index, Class<?> type, boolean asArray,
                                         List<Class<?>> hierarchy, Set<Class<?>> visited) {
            if (asArray) {
                type = Array.newInstance(type, 0).getClass();
            }
            if (visited.add(type)) {
                hierarchy.add(index, type);
            }
        }

        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder();
            builder.append("ConversionService converters =\n");
            for (String converterString : getConverterStrings()) {
                builder.append('\t').append(converterString).append('\n');
            }
            return builder.toString();
        }

        private List<String> getConverterStrings() {
            List<String> converterStrings = new ArrayList<String>();
            for (ConvertersForPair convertersForPair : converters.values()) {
                converterStrings.add(convertersForPair.toString());
            }
            Collections.sort(converterStrings);
            return converterStrings;
        }
    }


    /**
     * Manages converters registered with a specific {@link GenericConverter.ConvertiblePair}.
     */
    private static class ConvertersForPair {

        private final LinkedList<GenericConverter> converters = new LinkedList<GenericConverter>();

        public void add(GenericConverter converter) {
            this.converters.addFirst(converter);
        }

        public GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
            for (GenericConverter converter : this.converters) {
                if (!(converter instanceof ConditionalGenericConverter) ||
                        ((ConditionalGenericConverter) converter).matches(sourceType, targetType)) {
                    return converter;
                }
            }
            return null;
        }

        @Override
        public String toString() {
            return StringUtils.collectionToCommaDelimitedString(this.converters);
        }
    }


    /**
     * Internal converter that performs no operation.
     */
    private static class NoOpConverter implements GenericConverter {

        private final String name;

        public NoOpConverter(String name) {
            this.name = name;
        }

        public Set<ConvertiblePair> getConvertibleTypes() {
            return null;
        }

        public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
            return source;
        }

        @Override
        public String toString() {
            return this.name;
        }
    }

}
